package net.android.gainer;

import java.util.concurrent.TimeoutException;
import android.os.Handler;
import android.os.Message;
import android.util.Log;

/**
 * <p>Gainer class</p>
 * @author tomo＠android-group.jp
 * @version 0.1
 * @lisence New BSD
 */
final public class Gainer extends Handler {
	final static String TAG = "AndroidGainer";
	final static String VERSION = "1.";
	private static boolean DEBUG = false;

	// Gainer configuration
	/**
	 * Gainerのコンフィギュレーション1
	 */
	public final static int MODE1 = 1;
	
	/**
	 * Gainerのコンフィギュレーション2
	 */
	public final static int MODE2 = 2;
	
	/**
	 * Gainerのコンフィギュレーション3
	 */
	public final static int MODE3 = 3;
	
	/**
	 * Gainerのコンフィギュレーション4
	 */
	public final static int MODE4 = 4;
	
	/**
	 * Gainerのコンフィギュレーション5
	 */
	public final static int MODE5 = 5;
	
	/**
	 * Gainerのコンフィギュレーション6
	 */
	public final static int MODE6 = 6;
	
	/**
	 * Gainerのコンフィギュレーション7
	 */
	public final static int MODE7 = 7;
	
	/**
	 * Gainerのコンフィギュレーション8
	 */
	public final static int MODE8 = 8;
	
	private static final String BUTTON_PRESSED = "N*";
	private static final String BUTTON_RELEASED = "F*";
	private static final String ANALOG_INPUT = "I";
	private static final String DIGITAL_INPUT = "R";

	private String mPortName;
	private Serial mSerial;
	private boolean currentVerbose;
	private OnGainerListener gainerEvent;
	private boolean buttonPressed;
	private int[] analogInput;
	private boolean[] digitalInput;
	private int[] analogOutput;
	private boolean[] digitalOutput;

	/**
	 * <p>Construct Gainer with the <b>port name</b> and the <b>mode</b>.</p>
	 * <p>コンストラクタ。</p>
	 * 
	 * @param portName
	 *            <p>Gainer serial port name.</p>
	 *            <p>Gainerが使用するシリアルのポート名。 ex.) "/dev/ttyUSB0"</p>
	 * @param mode
	 *            <p>Gainer configuration.<p>
	 *            <p>Gainerのコンフィギュレーション番号。{1|2|...|8}</p>
	 * @param debug
	 *            <p>true:verbose mode / false:silent mode</p>
	 *            <p>trueでログ出力。</p>
	 */
	public Gainer(String portName, int mode, boolean debug) {
		this.mPortName = portName;
		this.mSerial = new Serial(this, portName, debug);
		this.DEBUG = debug;
		this.gainerEvent = null;
		this.buttonPressed = false;
		
		// Gainerとの接続を行う
		if (mSerial.connectGainer()) {
			LogI("ok.. connect gainer");
			sleep(100);
			initialize(mode, false);
		} else {
			errorMessage("Gainer not found !!");
			return;
		}

		mSerial.startPortInputListener();
	}

	/**
	 * <p>Same as "Gainer(portName, mode, <b>false</b>);"</p>
	 * 
	 * @param portName
	 *            <p>Gainer serial port name.</p>
	 *            <p>Gainerが使用するシリアルのポート名。 ex.) "/dev/ttyUSB0"</p>
	 * @param mode
	 *            <p>Gainer configuration.<p>
	 *            <p>Gainerのコンフィギュレーション番号。{1|2|...|8}</p>
	 */
	public Gainer(String portName, int mode) {
		this(portName, mode, false);
	}

	/**
	 * <p>This method call close();</p>
	 * <p>このメソッドで一応close()メソッドを呼出す。GC時にGainerオブジェクトが存在しない場合は自動でclose()してくれるはず。
	 * ユーザアプリでclose()を呼出しておく事を推奨。</p>
	 */
	public void finilize() {
		close();
	}

	/**
	 * <p>This should be called when Gainer finish.</p>
	 * <p>終了コマンドをGainerに送るメソッド。Gainer終了時に呼出す事を推奨。</p>
	 */
	public void close() {
		if (mSerial != null) {
			try {
				LogI("Gainer close");
				mSerial.sendGainer("Q*", false, false);
				mSerial.disconnectGainer();
			} catch (TimeoutException e) {
				e.printStackTrace();
			}
			mSerial = null;
			System.gc();
		}
	}

	/**
	 * Check firmware version and set configuration.
	 * 
	 * @param mode
	 *            Configuration number.
	 * @param verb
	 *            Gainer verbose mode.
	 */
	private void initialize(int mode, boolean verb) {
		String version = getDeviceVersion();
		if (version.startsWith(VERSION, 1)) {
			LogI("version check. OK");

			setVerbose(verb);
			if (!configuration(mode)) {
				errorMessage("configuration error!!");
			}

			sleep(100);

			LogI("get ready!");
			// /open
		}
	}

	/**
	 * Setting configuration
	 * 
	 * @param mode
	 *            Configuration number.
	 * @return success or not
	 */
	private boolean configuration(int mode) {
		boolean returnFlag = false;
		LogI("configuration: MODE" + mode);

		this.analogInput = null;
		this.digitalInput = null;
		switch (mode) {
		case MODE1:
			this.analogInput = new int[4];
			this.digitalInput = new boolean[4];

			this.analogOutput = new int[4];
			this.digitalOutput = new boolean[4];

			if (execCode("KONFIGURATION_1*", true, true).startsWith(
					"KONFIGURATION_1")) {
				returnFlag = true;
				break;
			} else {
				errorMessage("configuration error:" + mode);
			}
		case MODE2:
			analogInput = new int[8];
			digitalInput = new boolean[0];

			analogOutput = new int[4];
			digitalOutput = new boolean[4];

			if (execCode("KONFIGURATION_2*", true, true).startsWith(
					"KONFIGURATION_2")) {
				returnFlag = true;
				break;
			} else {
				errorMessage("configuration error:" + mode);
			}
		case MODE3:
			analogInput = new int[4];
			digitalInput = new boolean[4];

			analogOutput = new int[8];
			digitalOutput = new boolean[0];

			if (execCode("KONFIGURATION_3*", true, true).startsWith(
					"KONFIGURATION_3")) {
				returnFlag = true;
				break;
			} else {
				errorMessage("configuration error:" + mode);
			}
		case MODE4:
			analogInput = new int[8];
			digitalInput = new boolean[0];

			analogOutput = new int[8];
			digitalOutput = new boolean[0];

			if (execCode("KONFIGURATION_4*", true, true).startsWith(
					"KONFIGURATION_4")) {
				returnFlag = true;
				break;
			} else {
				errorMessage("configuration error:" + mode);
			}
		case MODE5:
			analogInput = new int[0];
			digitalInput = new boolean[16];

			analogOutput = new int[0];
			digitalOutput = new boolean[0];

			if (execCode("KONFIGURATION_5*", true, true).startsWith(
					"KONFIGURATION_5")) {
				returnFlag = true;
				break;
			} else {
				errorMessage("configuration error:" + mode);
			}
		case MODE6:
			analogInput = new int[0];
			digitalInput = new boolean[0];

			analogOutput = new int[0];
			digitalOutput = new boolean[16];

			if (execCode("KONFIGURATION_6*", true, true).startsWith(
					"KONFIGURATION_6")) {
				returnFlag = true;
				break;
			} else {
				errorMessage("configuration error:" + mode);
			}
		case MODE7:
			analogInput = new int[0];
			digitalInput = new boolean[8];

			analogOutput = new int[0];
			digitalOutput = new boolean[8];

			if (execCode("KONFIGURATION_7*", true, true).startsWith(
					"KONFIGURATION_7")) {
				returnFlag = true;
				break;
			} else {
				errorMessage("configuration error:" + mode);
			}
		case MODE8:
			analogInput = new int[0];
			digitalInput = new boolean[0];

			analogOutput = new int[8];
			digitalOutput = new boolean[8];

			if (execCode("KONFIGURATION_8*", true, true).startsWith(
					"KONFIGURATION_8")) {
				returnFlag = true;
				break;
			} else {
				errorMessage("configuration error:" + mode);
			}
		}

		// initialize all arrays.
		for (int i = 0; i < analogInput.length; i++) {
			analogInput[i] = 0;
		}
		for (int i = 0; i < digitalInput.length; i++) {
			digitalInput[i] = false;
		}
		for (int i = 0; i < analogOutput.length; i++) {
			analogOutput[i] = 0;
		}
		for (int i = 0; i < digitalOutput.length; i++) {
			digitalOutput[i] = false;
		}

		return returnFlag;
	}

	/**
	 * Set Gainer verbose mode.
	 * 
	 * @param verb
	 *            <p>
	 *            true:verbose mode
	 *            </p>
	 *            <p>
	 *            false:silent mode
	 *            </p>
	 */
	private void setVerbose(boolean verb) {
		this.currentVerbose = verb;
		String code = "";
		if (verb) {
			code = "V1*";
			LogI("verbose mode");
		} else {
			code = "V0*";
			LogI("silent mode");
		}
		execCode(code, true, true);

		sleep(100);// setup time of gainer device

	}

	/**
	 * Send a <b>code</b> to Gainer.
	 * 
	 * @param code
	 *            Gainer command.
	 * @param answer
	 *            true:return Gainer's echo.
	 * @param receiveResponse
	 *            receive gainer's response.
	 * @return
	 */
	String execCode(String code, boolean answer, boolean receiveResponse) {
		String returnCode = "";
		try {
			if (DEBUG) {
				LogI("exec Code " + code);
			}
			returnCode = mSerial.sendGainer(code, answer, receiveResponse);
		} catch (TimeoutException e) {
			e.printStackTrace();
			LogE("exec Code " + code + " TIMEOUT!");
		}
		return returnCode;
	}

	/**
	 * <p>Set a gainer event listener.</p>
	 * <p>Gainerのイベントリスナを設定する。</p>
	 */
	public void setOnGainerListener(OnGainerListener gainerEvent) {
		this.gainerEvent = gainerEvent;
	}

	// -----------------------------------------------------------------------Handling
	// Gainer Events
	/**
	 * <p>Receive the gainer event, and fire event(s).</p>
	 * <p>ユーザがこのメソッドを使う事は無い。</p>
	 */
	final public void handleMessage(Message msg) {
		String orgcode = msg.obj.toString();

		if (orgcode == null || gainerEvent == null) {
			return;
		}

		String code = orgcode.substring(0, 2);

		if (code.compareTo(BUTTON_PRESSED) == 0) {
			gainerEvent.onPressed();
			buttonPressed = true;
		} else if (code.compareTo(BUTTON_RELEASED) == 0) {
			gainerEvent.onReleased();
			buttonPressed = false;
		} else if (orgcode.startsWith(ANALOG_INPUT)) {
			setAnalogInput(orgcode);
			gainerEvent.gainerAnalogInputUpdated(analogInput);
		} else if (orgcode.startsWith(DIGITAL_INPUT)) {
			setDigitalInput(orgcode);
			gainerEvent.gainerDigitalInputUpdated(digitalInput);
		}
	}

	/**
	 * <p>Get the state of being button pressed</p>
	 * <p>ボタンの状態を取得する。</p>
	 * 
	 * @return true: the button is pressed, false: the button is released.
	 */
	public boolean buttonPressed() {
		return buttonPressed;
	}

	/**
	 * <p>Get the array of digital inputs.</p>
	 * <p>デジタル入力配列を取得する。</p>
	 * 
	 * @return digital inputs array
	 */
	public boolean[] digitalInput() {
		return digitalInput;
	}

	/**
	 * <p>Get the array of analog inputs.</p>
	 * <p>アナログ入力配列を取得する。</p>
	 * 
	 * @return analog inputs array
	 */
	public int[] analogInput() {
		return analogInput;
	}

	/**
	 * <p>Turn on the LED on Gainer I/O.</p>
	 * <p>LEDを点灯する。</p>
	 */
	public void turnOnLED() {
		execCode("h*", currentVerbose, false);
	}

	/**
	 * <p>Turn off the LED on Gainer I/O.</p>
	 * <p>LEDを消灯する。</p>
	 * 
	 */
	public void turnOffLED() {
		execCode("l*", currentVerbose, false);
	}

	/**
	 * <p>Set digital inputs to digitalInputs array. You can get new digital inputs
	 * through digitalInputs();</p>
	 * <p>新たなデジタル入力をGainerから取得する。
	 * 値はdigitalInputs();を用いて取得する。</p>
	 */
	public void peekDigitalInput() {
		execCode("R*", false, false);
	}

	/**
	 * <p>Continue setting digital inputs to digitalInputs array.</p>
	 * <p>デジタル入力を取得し続ける。取得間隔は30ms.</p>
	 */
	public void beginDigitalInput() {
		mSerial.beginDigitalInput();
	}

	/**
	 * <p>Continue setting digital inputs to digitalInputs array.</p>
	 * <p>デジタル入力を取得し続ける。ポーリング時間指定[ms]</p>
	 */
	public void beginDigitalInput(long interval) {
		mSerial.beginDigitalInput(interval);
	}

	
	/**
	 * <p>Finish setting digital inputs continuously.</p>
	 * <p>デジタル入力の連続取得を終了する。</p>
	 */
	public void endDigitalInput() {
		mSerial.endDigitalInput();
	}

	/**
	 * <p>Output digital port. from 0x0000 to 0xFF. The state of all digital
	 * output ports are ON is as follow, 0xFF = 1111|1111|1111|1111 (16ports)</p>
	 * <p>デジタル出力を行う。int値をビット演算する。1で出力し、0で出力しない。小さい桁から順にポート番号の小さい順となる。</p>
	 * <p>例：values=0x02 => 0000|0000|0000|0010(第2ポートのみデジタル出力する)</p>
	 * 
	 * @param values
	 *            The value is between 0x00 and 0xFF. The maximum number of
	 *            ports is 16.
	 * @return
	 * @TODO Don't test yet.
	 */
	public boolean digitalOutput(int values) {
		if (values <= 0xFFFF && values >= 0) {
			String val = Integer.toHexString(values).toUpperCase();
			String sv = "";
			// 必ず4桁
			for (int i = 0; i < 4 - val.length(); i++) {
				sv += "0";
			}
			sv += val;

			String code = "D" + sv + "*";
			execCode(code, false, false);

			for (int i = 0; i < digitalOutput.length; i++) {
				int a = (values >> i) & 1;
				digitalOutput[i] = a == 1 ? true : false;
			}

			return true;
		}
		return false;
	}

	/**
	 * <p>Output digital port.</p>
	 * <p>デジタル出力。要素がtrueとなる配列番号のポートにデジタル出力する。</p>
	 * 
	 * @param values
	 *            The maximum length of values[] is 16.
	 * @return true: success, false: unsuccess.
	 * @TODO Don't test yet.
	 */
	public boolean digitalOutput(boolean[] values) {
		int channels = 0;
		if (digitalOutput.length == values.length) {
			for (int i = 0; i < values.length; i++) {
				if (values[i]) {
					/* trueである要素のbitを1にする */
					channels |= (1 << i);
				}
				digitalOutput[i] = values[i] ? true : false;
			}
		} else {
			return false;
		}

		/* 出力bitが重ね合わされたchannelsを16進数表記に変換する */
		String val = Integer.toHexString(channels).toUpperCase();
		String sv = "";
		for (int i = 0; i < digitalOutput.length - val.length(); i++) {
			/* 桁数を合わせる */
			sv += "0";
		}
		sv += val;

		String code = "D" + sv + "*";
		execCode(code, false, false);
		return true;

	}

	/**
	 * <p>Set the digital array[<b>ch</b>] high.</p>
	 * <p>ポート番号(ch)のデジタル出力をonにする。</p>
	 * 
	 * @param ch
	 *            target channel
	 * @return true is success
	 */
	public boolean setHigh(int ch) {
		if (digitalOutput.length > ch) {
			String code = "H" + Integer.toHexString(ch).toUpperCase() + "*";
			execCode(code, false, false);
			digitalOutput[ch] = true;
			return true;
		}
		return false;
	}

	/**
	 * <p>The digital <b>channels</b> array are high, others are low.</p>
	 * <p>ポート番号(channels)のデジタル出力をonにする。</p>
	 * 
	 * @param channels
	 * @return true is success
	 */
	public boolean setHigh(int[] channels) {
		byte vch = 0;
		for (int i = 0; i < channels.length; i++) {
			if (digitalOutput.length > channels[i]) {
				vch |= (1 << channels[i]);
			} else {
				return false;
			}
			digitalOutput[channels[i]] = true;
		}

		String val = Integer.toHexString(vch).toUpperCase();
		String sv = "";
		for (int i = 0; i < digitalOutput.length - val.length(); i++) {
			sv += "0";
		}
		sv += val;

		String code = "D" + sv + "*";
		execCode(code, false, false);
		return true;
	}

	/**
	 * <p>Set the digital array[<b>ch<b>] low.</p>
	 * <p>ポート番号(ch)のデジタル出力をoffにする。</p>
	 * 
	 * @param ch
	 * @return true is success
	 */
	public boolean setLow(int ch) {
		if (digitalOutput.length > ch) {
			String code = "L" + Integer.toHexString(ch).toUpperCase() + "*";
			execCode(code, false, false);

			digitalOutput[ch] = false;
			return true;
		}
		return false;
	}

	/**
	 * <p>Set the digital <b>channels</b> array are high, others are low.</p>
	 * <p>ポート番号(channels)のデジタル出力をoffにする。</p>
	 * 
	 * @param channels
	 * @return true is success
	 */
	public boolean setLow(int[] channels) {
		byte vch = 0;
		for (int i = 0; i < channels.length; i++) {
			if (digitalOutput.length > channels[i]) {
				vch |= (1 << channels[i]);
			} else {
				return false;
			}
			digitalOutput[channels[i]] = false;
		}

		String val = Integer.toHexString(vch).toUpperCase();
		String sv = "";
		for (int i = 0; i < digitalOutput.length - val.length(); i++) {
			sv += "0";
		}
		sv += val;

		String code = "D" + sv + "*";
		execCode(code, false, false);
		return true;
	}
	
	/**
	 * code must start with "R" or "r".
	 * 
	 * @param code
	 */
	void setDigitalInput(String code) {
		int value = Integer.parseInt(code.substring(1,5),16);
		for(int i=0;i<digitalInput.length;i++){
		    int c = 1&(value>>i);
		    if(c==1){
			digitalInput[i] = true;
		    }else{
			digitalInput[i] = false;
		    }
		}
	}

	/**
	 * <p>Set analog inputs to analogInputs array. You can get new analog inputs
	 * through analogInputs();</p>
	 * <p>現在のGainerのアナログ入力を取得する。値の取得はanaloginputs()で行う。</p>
	 */
	public void peekAnalogInput() {
		execCode("I*", false, false);
	}

	/**
	 * <p>Continue setting analog inputs to analogInputs array.</p>
	 * <p>アナログ入力を取得し続ける。</p>
	 */
	public void beginAnalogInput() {
		mSerial.beginAnalogInput();
	}

	/**
	 * <p>Continue setting analog inputs to analogInputs array.</p>
	 * <p>アナログ入力を取得し続ける。ポーリング時間指定[ms]</p>
	 */
	public void beginAnalogInput(long interval) {
		mSerial.beginAnalogInput(interval);
	}
	
	/**
	 * <p>Finish setting analog inputs continuously.</p>
	 * <p>連続アナログ入力を終了する。</p>
	 */
	public void endAnalogInput() {
		mSerial.endAnalogInput();
	}

	/**
	 * 指定チャンネルにアナログ出力
	 * 
	 * @param ch : 出力するチャンネル
	 * @param values : 0 - 255
	 * @return true : 成功
	 * @return false : 失敗
	 */
	public boolean analogOutput(int ch,int value){
		if(ch>=analogOutput.length ){
			return false;
		}
		String code = "a" + Integer.toHexString(ch).toUpperCase();
		value = value<  0 ?   0: value;
		value = value>255 ? 255: value;
//	冗長かな？
		analogOutput[ch] = value;
		
		String sv = value<16 ? "0": "";
		sv+= Integer.toHexString(value).toUpperCase();
		code+=sv;
		code+="*";
		
		execCode(code,currentVerbose,false);
		return true;
	}
	
	/**
	 * 配列を対応するチャンネルにそれぞれアナログ出力
	 * 
	 * @param values : 0 - 255
	 * @return true : 成功
	 * @return false : 失敗
	 */
	public boolean analogOutput(int[] values){
  	String code = "A";
  	String sv="";
		if(analogOutput.length==values.length){
			for(int i=0;i<values.length;i++){
				values[i] = values[i]<  0 ?   0: values[i];
				values[i] = values[i]>255 ? 255: values[i];
				analogOutput[i] = values[i];
				
				sv = values[i]<16 ? "0": "";
				sv+= Integer.toHexString(values[i]).toUpperCase();
				code+=sv;
			}
		  code+= "*";
		}else{
			return false;
		}
		execCode(code,currentVerbose,false);
		return true;
	}
	
	/**
	 * code must start with "I" or "i".
	 * 
	 * @param code
	 */
	void setAnalogInput(String code) {
		String value;
		for (int i = 0; i < analogInput.length; i++) {
			value = code.substring(2 * i + 1, 2 * (i + 1) + 1);
			analogInput[i] = Integer.parseInt(value, 16);
		}
	}

	/**
	 * Get device firmware version
	 * 
	 * @return device firmware version
	 */
	private String getDeviceVersion() {
		return execCode("?*", true, true);
	}

	static void sleep(long time) {
		try {
			Thread.sleep(time);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Output log and eixt.
	 * 
	 * @param msg
	 *            Error message.
	 */
	private void errorMessage(String msg) {
		LogE(msg);
		System.exit(-1);
	}

	/**
	 * Output error or exception message.
	 * 
	 * @param msg
	 *            The message.
	 */
	static void LogE(String msg) {
		Log.e(TAG, msg);
	}

	/**
	 * Output information message for debugging or something.
	 * 
	 * @param msg
	 *            The message.
	 */
	static void LogI(String msg) {
		Log.i(TAG, msg);
	}
}
